文藝復興的畫室,是一個分工細密的組織
師傅負責構圖、定調;資深學徒處理背景與衣褶;新來的學徒,負責磨顏料、打底這套分工能運作,靠一條不成文的默契
每個人做好自己那一部分,不多管別人的畫布,也不用擔心自己的畫布被別人亂動模組四處理的是「贅肉」,模組五不一樣,處理的是「關係」
類別與類別之間,該保持多少距離,才不會綁死彼此耦合本身不是壞事,問題出在耦合的方式不對
四個「關係」警訊:
| Day | Code Smell | 一句話定位 |
|---|---|---|
| 26 | 依戀情結 (Feature Envy) | 一個方法,對別的類別的資料,比對自己的資料還熟 |
| 27 | 不適當的親密關係 (Inappropriate Intimacy) | 兩個類別,深入了解彼此到不該知道的程度 |
| 28 | 訊息鏈 (Message Chains) | 一句請求,要經過好幾手,才能送到真正該處理它的人手上 |
| 29 | 中間人 (Middle Man) | 一個角色,只負責轉手,自己不做任何判斷 |
最後一個模組,第一站,從最容易被忽略的一種開始
畫室分配給每位學徒固定的工作區
自己的顏料、自己的畫架,理論上,一整天的時間都該花在自己分內的畫布上
但如果某位學徒,每天大半時間都泡在隔壁畫架旁,幫別人調顏色、挑筆刷,自己的畫布反而空在那裡沒進度
旁人多半會問一句:「這個人,到底是誰的學徒?」
系統裡有一個 OrderProcessor,其中一個方法,是拼出客戶的會員徽章文字:
public class Customer
{
public string Name { get; set; }
public CustomerTier Tier { get; set; }
public decimal TotalSpend { get; set; }
public int LoyaltyPoints { get; set; }
}
public class OrderProcessor
{
public string BuildLoyaltyBadge(Customer customer)
{
string tierLabel = customer.Tier == CustomerTier.Vip ? "VIP" : "一般會員";
string spendLabel = customer.TotalSpend > 100000
? "鑽石"
: customer.TotalSpend > 50000
? "黃金"
: "一般";
return $"{tierLabel}・{spendLabel}・{customer.LoyaltyPoints}點";
}
}
這個方法叫 BuildLoyaltyBadge,寫在 OrderProcessor 裡
但整段邏輯裡,完全沒有用到 OrderProcessor 自己的任何資料
它做的每一件事,都是在讀 customer 的欄位Tier、TotalSpend、LoyaltyPoints 一個接一個地問
這個方法讀起來,更像是屬於 Customer 的一部分,而不是 OrderProcessor 的一部分
它帶來幾個實際的麻煩:
Customer 的內部欄位,只要調整一個名稱,就得回頭修改 OrderProcessor 裡這段完全不相關的程式碼Customer,卻要繞到 OrderProcessor 才找得到答案
OrderProcessor 承擔了一份不屬於它的知識,它原本該專心處理訂單,卻要順便搞懂會員分級的所有細節這正是依戀情結的典型症狀:一個方法,對別人的資料比對自己的資料還熟
它天天往隔壁畫架跑,自己畫布上的工作,反而沒人記得它原本該做什麼
把銘傳大學還給劉銘傳把亞洲大學還給亞洲統神把成功大學還給鄭成功把逢甲大學還給丘逢甲把中正大學還給蔣中正把大同大學還給葉大同
解法是搬移方法 (Move Method):把這段邏輯,整段搬到它真正依戀的 Customer 裡
public class Customer
{
public string Name { get; set; }
public CustomerTier Tier { get; set; }
public decimal TotalSpend { get; set; }
public int LoyaltyPoints { get; set; }
public string BuildLoyaltyBadge()
{
string tierLabel = Tier == CustomerTier.Vip ? "VIP" : "一般會員";
string spendLabel = TotalSpend > 100000
? "鑽石"
: TotalSpend > 50000
? "黃金"
: "一般";
return $"{tierLabel}・{spendLabel}・{LoyaltyPoints}點";
}
}
OrderProcessor 現在只需要告知 Customer 去做這件事,不再自己動手:
public class OrderProcessor
{
public string BuildLoyaltyBadge(Customer customer) => customer.BuildLoyaltyBadge();
}
這是「Tell, Don't Ask」原則的具體展現
與其向 Customer 問一堆欄位、自己在外面拼湊答案
不如直接告訴 Customer:「你自己算出你的徽章」
這個壞味道,很少是一開始就故意寫成這樣的
最常見的成因,是先前做過一次資料重構
例如把散落的欄位提煉成一個新類別(呼應 Day 05 的原始型別執念、Day 07 的資料泥團)
卻只搬走了資料,忘了把操作這些資料的行為一起搬過去
資料搬家了,行為卻留在原地,繼續伸長手去操作已經搬走的資料
這條線,就是依戀情結的耦合
.Get 之後自己組裝結果,而不是直接告訴物件去做?明天我們看更嚴重的一種黏著:兩個類別,不只互相熟悉,還深入了解對方的每一個內部細節
模組五第二站:不適當的親密關係(Inappropriate Intimacy)